12  Pandas 数据框拼接

12.1 引言数据整合的必要性

在实际金融分析中,数据往往分散在多个文件或数据源中:

  • 不同时期的数据分开存储
  • 不同市场的数据分别管理
  • 不同频率的数据需要整合

Pandas提供了强大的数据拼接工具,使得数据整合变得简单高效。

12.2 本章学习目标

通过本章学习,你将能够:

  1. 说出 concatmergejoin 三类拼接接口的分工:轴向堆叠、按键连接、按索引连接
  2. 使用 pd.concataxisjoinkeysignore_index 参数完成纵向/横向拼接与来源标识
  3. 使用 pd.mergehow(inner/outer/left/right)与 left_index/right_index 完成数据库风格连接
  4. 根据索引与列的对齐情况,判断拼接结果中 NaN 出现的位置与原因

先修内容:第 章节 10 章、第 章节 11 章的数据框创建与内部操作。

12.3 concat轴向拼接

pd.concat()函数用于沿指定轴拼接多个数据框。

任务要求:从给定 Excel 地址的 Sheet1 读取股价数据并按位置切成前 100 行(df1)与其余行(df2),从 Sheet2 读取成交量数据;用 concat 纵向拼回 df1df2 并输出全年交易数据条数;再用 merge 按日期索引合并收盘价与成交量,取出 2022-06-01 一行,输出该日收盘价与成交量(万股)。请将代码原样输入教学平台(注释除外),判定以平台为准。

列表 12.1: 平台原始代码
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库
# 从Excel文件读取数据存入df_price
df_price=pd.read_excel('https://huoran.oss-cn-shenzhen.aliyuncs.com/20230306/xlsx/1632680830600503296.xlsx',index_col=0,sheet_name='Sheet1')
#df1 包含前 100 行,df2 包含剩余的行
df1=df_price.iloc[:100,:] 
df2=df_price.iloc[100:,:]  # 按位置索引提取从Excel文件读取数据存入df_price
# 从Excel文件读取数据存入tencent_vol
tencent_vol=pd.read_excel('https://huoran.oss-cn-shenzhen.aliyuncs.com/20230306/xlsx/1632680830600503296.xlsx',index_col=0,sheet_name='Sheet2')

df_concat=pd.concat([df1,df2],axis=0)  # 拼接数据框并存入df_concat
print(f'2022年有{df_concat.shape[0]}条交易数据')  # 输出2022年有

# 合并数据框并存入df_merge
df_merge=pd.merge(left=df_price['腾讯控股'],right=tencent_vol,right_index=True,left_index=True)
stock_data=df_merge.loc['2022-06-01',:]  # 按标签索引提取数据
print(f"这个交易日的收盘价为{stock_data[0]}元,成交量为{stock_data[2]/10000}万股")  # 输出这个交易日的收盘价为

预期输出(数据来自教学平台的 OSS 直链,本机实测判读要点如下;具体数值以平台运行结果为准):

  • 第一行输出 2022年有N条交易数据,N 为 Sheet1 前后两段拼回后的总行数(作者实测环境为 246)
  • 第二行输出 这个交易日的收盘价为X元,成交量为Y万股,作者实测 2022-06-01 腾讯控股收盘价 360.4 元、成交量 2692.0272 万股
  • 合并结果按日期索引对齐,stock_data[0] 是收盘价、stock_data[2] 是成交量(股),除以 10000 换算为万股

concat参数详解:

  • objs: 要拼接的数据框列表
  • axis: 0=垂直拼接(行), 1=水平拼接(列)
  • join: ‘inner’(交集), ‘outer’(并集,默认)
  • ignore_index: 是否忽略原索引重新编号
  • keys: 创建层次化索引标识来源

12.4 merge数据库风格连接

pd.merge()类似于SQL的JOIN操作,基于共同列或索引合并数据。

列表 12.2: 使用merge合并DataFrame
# =============================================================================
# 题目:merge函数基于索引合并价格与成交量数据
# =============================================================================
# 在金融分析中,经常需要将不同数据表的信息整合在一起,例如将股价数据与
# 成交量数据合并,以便进行量价分析。本示例演示如何使用pd.merge()函数
# 基于日期索引将腾讯控股的收盘价和成交量数据进行内连接合并。

# ==================== 定义数据来源URL ====================
url = 'https://huoran.oss-cn-shenzhen.aliyuncs.com/20230306/xlsx/1632680830600503296.xlsx'  # 与上方代码块使用相同的Excel文件地址

# ==================== 读取成交量数据 ====================
# 从同一Excel文件的Sheet2读取成交量数据
tencent_vol = pd.read_excel(url, index_col=0, sheet_name='Sheet2')

# ==================== 基于索引进行内连接合并 ====================
# pd.merge()实现类似SQL的JOIN操作:
# left:左表,取df_price中的'腾讯控股'列(收盘价)
# right:右表,取tencent_vol(成交量数据)
# left_index=True/right_index=True:使用左右两表的索引(日期)作为连接键
# how='inner':内连接,只保留两个表中索引都存在的日期
df_merge = pd.merge(
    left=df_price['腾讯控股'],  # 左表:腾讯控股收盘价序列
    right=tencent_vol,           # 右表:成交量数据
    left_index=True,             # 布尔值,指定使用左表的索引作为连接键
    right_index=True,            # 布尔值,指定使用右表的索引作为连接键
    how='inner'                  # 连接方式,'inner'表示只保留匹配的行
)

# ==================== 输出合并后的数据 ====================
print("合并后的数据:")  # 打印标题
print(df_merge.head())  # 显示前5行数据,包含收盘价和成交量

# ==================== 查询特定日期的交易数据 ====================
# 使用.loc通过日期索引定位特定行的数据
stock_data = df_merge.loc['2022-06-01', :]  # 获取2022-06-01这一行的所有列数据
print(f"\n2022-06-01的交易数据:")  # 打印日期标题
print(f"  收盘价: {stock_data[0]:.2f}元")  # 输出收盘价,保留2位小数
print(f"  成交量: {stock_data[2]/10000:.2f}万股")  # 输出成交量,转换为万股单位

merge参数详解:

  • left, right: 要合并的两个数据框
  • how: 连接方式
    • 'inner': 内连接(交集,默认)
    • 'outer': 外连接(并集)
    • 'left': 左连接(保留左表所有行)
    • 'right': 右连接(保留右表所有行)
  • on: 用于连接的列名
  • left_on, right_on: 左右表不同的列名

12.5 join索引合并

join()是merge的简化版,专门用于基于索引合并。

列表 12.3: 使用join进行索引合并
# =============================================================================
# 题目:join函数简化索引合并操作
# =============================================================================
# join()是merge()的便捷方法,专门用于基于索引合并数据框,语法更加简洁。
# 本示例演示如何使用join()方法将腾讯控股的收盘价与成交量数据合并,
# 相比merge(),join()更适合处理基于索引的合并场景。

# ==================== 使用join进行索引合并 ====================
# df_price[['腾讯控股']]:从价格数据框中提取'腾讯控股'列
# .join(tencent_vol):将成交量数据框基于索引合并到收盘价数据框
# how='inner':内连接,只保留两个表中索引都存在的日期
df_join = df_price[['腾讯控股']].join(  # 调用左表的join方法
    tencent_vol,                        # 要合并的右表(成交量数据)
    how='inner'                         # 连接方式,内连接保留匹配的索引
)

# ==================== 输出合并结果 ====================
print("使用join合并:")  # 打印提示信息
print(df_join.head())  # 显示前5行数据,包含收盘价和成交量

12.6 拼接的金融应用场景

12.6.1 1. 多时期数据整合

列表 12.4: 拼接不同时期的数据
# =============================================================================
# 题目:concat拼接不同月份的股价数据
# =============================================================================
# 在金融时间序列分析中,经常需要将不同时期的数据(如月度数据、季度数据)
# 拼接成更长时间序列以便进行趋势分析。本示例演示如何使用concat()将1月
# 和2月的股价数据垂直拼接成Q1(第一季度)完整数据。

# ==================== 创建1月数据 ====================
# 使用字典创建DataFrame,包含价格和成交量两列
jan_data = pd.DataFrame({
    'price': [10, 11, 12],                    # 股价数据,表示3个交易日的收盘价
    'volume': [1000, 1100, 1200]              # 成交量数据,单位可能是手或股
}, index=pd.date_range('2024-01-01', periods=3))  # 创建日期范围索引,从2024-01-01开始的3天

# ==================== 创建2月数据 ====================
# 创建2月的交易数据,结构相同
feb_data = pd.DataFrame({
    'price': [13, 14, 15],                    # 2月的股价数据,显示上升趋势
    'volume': [1300, 1400, 1500]              # 2月的成交量,呈现增长态势
}, index=pd.date_range('2024-02-01', periods=3))  # 2月日期索引

# ==================== 垂直拼接两月数据 ====================
# pd.concat()默认axis=0,将feb_data追加到jan_data下方
# 拼接后形成包含1月和2月数据的Q1时间序列
q1_data = pd.concat([jan_data, feb_data])  # 垂直拼接,自动对齐列名
print("Q1数据:")  # 打印标题
print(q1_data)    # 输出完整的Q1数据,包含6个交易日

12.6.2 2. 多市场数据整合

列表 12.5: 合并不同市场的数据
# =============================================================================
# 题目:concat拼接沪市和深市的股票数据
# =============================================================================
# 在A股市场中,股票分为上海证券交易所(沪市)和深圳证券交易所(深市)。
# 实际分析中常需要将两个市场的数据整合在一起进行跨市场比较分析。
# 本示例演示如何使用concat()的keys参数为拼接后的数据添加层次化索引,
# 标识每条数据来源于哪个市场。

# ==================== 创建沪市股票数据 ====================
# 构造包含股票代码、名称和价格的DataFrame
sh_data = pd.DataFrame({
    'code': ['600000.SH', '600036.SH'],      # 股票代码,.SH后缀表示沪市
    'name': ['浦发银行', '招商银行'],        # 股票名称
    'price': [10.5, 45.2]                    # 股价,单位为元
})

# ==================== 创建深市股票数据 ====================
# 构造深市股票数据,结构与沪市相同
sz_data = pd.DataFrame({
    'code': ['000001.SZ', '000002.SZ'],      # 股票代码,.SZ后缀表示深市
    'name': ['平安银行', '万科A'],           # 股票名称
    'price': [12.3, 8.5]                     # 股价,单位为元
})

# ==================== 垂直拼接并添加市场标识 ====================
# pd.concat()的keys参数为拼接后的数据创建层次化索引
# keys=['沪市', '深市']:为两个数据框分别添加标签
# names=['市场']:指定层次化索引的级别名称
all_stocks = pd.concat([sh_data, sz_data], keys=['沪市', '深市'], names=['市场'])
print("两市股票:")  # 打印标题
print(all_stocks)    # 输出包含市场标识的合并数据,可通过层次化索引区分市场

12.7 本章小结

要点:

  • concat 沿轴堆叠多个数据框,axis=0 纵向、axis=1 横向,keys 可标识数据来源
  • merge 按(索引)键连接,how 决定保留交集、并集还是单侧全集
  • join 是基于索引合并的便捷写法,适合 left_index=True, right_index=True 的场景
  • 列名不对齐(concat)或索引不匹配(merge)的位置都会以 NaN 补位

易错点:

  • concat 默认 join='outer',两表列或索引不完全一致时,结果中会出现 NaN
  • 位置切片 iloc[:100, :]iloc[100:, :] 恰好无缝衔接,边界写错会产生重复或遗漏
  • 按位置取 stock_data[0]stock_data[2] 依赖列的先后顺序,列序一变就取错列

12.8 动手与思考

以下练习每题附参考答案(默认折叠)。请先独立完成并写下你的判断,再点开对照,最后上机验证。

  1. 输出预测:不运行代码,先写出下面代码的输出结果,再上机检验你的判断。

    import pandas as pd
    
    a = pd.DataFrame({'x': [1, 2]}, index=['r1', 'r2'])
    b = pd.DataFrame({'y': [3, 4]}, index=['r2', 'r3'])
    print(pd.concat([a, b], axis=1))
    print(pd.merge(a, b, left_index=True, right_index=True, how='inner'))
    print(pd.merge(a, b, left_index=True, right_index=True, how='outer'))

    参考答案(先写下你的预测再点开)

    解题思路:逐行推演。第 1 行 concat(axis=1) 横向拼接、默认 join='outer':行索引取两表并集 r1、r2、r3a 没有 y 列、b 没有 x 列,对不上的位置补 NaN——r1yNaNr3xNaN;出现 NaN 后整列被提升为 float(1 变 1.0)。第 2 行 merge(how='inner') 按索引做内连接:只保留两表索引的交集 r2,输出仅 1 行,且因没有任何缺失,xy 保持整数类型。第 3 行 merge(how='outer') 外连接保留索引并集,输出 3 行,只在单侧出现的行其另一侧字段为 NaN——结果与第 1 行数值相同,但来源不同:一个是“列对不齐补 NaN”,一个是“索引不匹配补 NaN”。

    # 验证脚本:逐行输出三个拼接/连接表达式的结果
    import pandas as pd  # 导入Pandas库
    
    a = pd.DataFrame({'x': [1, 2]}, index=['r1', 'r2'])  # 左表:x列,行索引r1、r2
    b = pd.DataFrame({'y': [3, 4]}, index=['r2', 'r3'])  # 右表:y列,行索引r2、r3,与左表部分重叠
    print(pd.concat([a, b], axis=1))  # 横向拼接:行索引取并集,对不上的位置补NaN
    print(pd.merge(a, b, left_index=True, right_index=True, how='inner'))  # 内连接:只保留共有索引r2
    print(pd.merge(a, b, left_index=True, right_index=True, how='outer'))  # 外连接:保留索引并集,单侧行补NaN

    预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):

          x    y
    r1  1.0  NaN
    r2  2.0  3.0
    r3  NaN  4.0
        x  y
    r2  2  3
          x    y
    r1  1.0  NaN
    r2  2.0  3.0
    r3  NaN  4.0

    回扣本章:对应本章小结“要点”第 2、4 条——how 决定保留交集还是并集;列名不对齐(concat)或索引不匹配(merge)的位置都以 NaN 补位。

  2. 概念辨析:concatmerge 分别解决什么问题?innerouter 连接的差别如何体现在输出行数上?

    参考答案(点开前请先独立完成)

    解题思路:逐点作答。第一,分工:concat 解决“轴向堆叠”——把多张表沿行(axis=0,纵向摞高)或沿列(axis=1,横向拼宽)直接摞起来,不要求任何键匹配,只靠行索引与列名对齐,对不上的位置补 NaN,适合“同结构数据分期分表存储”的整合;merge 解决“按键连接”——像 SQL 的 JOIN 一样按共同列或索引匹配行,把同一观测分散在两张表的字段拼到一行,适合“价格表与成交量表各有一份、按日期对上”的场景。第二,innerouter 体现在行数上:inner 只保留两表键都存在的行(交集),输出行数不超过较小一张表的行数;outer 保留两表全部键(并集),输出行数等于键并集的大小,只在单侧出现的键其在另一侧的字段为 NaN。以第 1 题为例:inner 只剩 r2 一行,outerr1、r2、r3 三行。

    回扣本章:对应本章小结“要点”第 1、2 条——concatmergejoin 三类接口的分工分别是轴向堆叠、按键连接、按索引连接,how 决定交集、并集还是单侧全集。

  3. 变式任务(平台任务同型改造):构造 1 月、2 月各 3 行的股价表,纵向拼接后用 keys=['1月', '2月'] 标识来源,再用 .loc['1月'] 取出 1 月子表。

    参考答案(点开前请先独立完成)

    解题思路:仿照 列表 12.1列表 12.4 的用法:分别构造 1 月、2 月各 3 行的收盘价表(行索引用日期,两个月日期互不重叠,纵向拼接不会产生 NaN);pd.concat([两表], keys=['1月', '2月']) 在纵向堆叠的同时为每段数据加上外层标签,形成“月份 + 日期”两层行索引;.loc['1月'] 只给外层标签,即取出 1 月那张子表,返回的子表行索引自动退回单层日期。下方代码中的价格为自选演示数值。

    # 变式程序:两个月股价表纵向拼接并用keys标识来源
    import pandas as pd  # 导入Pandas库
    
    jan_prices = pd.DataFrame({'收盘价': [10.0, 10.5, 11.0]}, index=pd.date_range('2024-01-01', periods=3))  # 1月3个交易日的收盘价
    feb_prices = pd.DataFrame({'收盘价': [11.5, 12.0, 12.5]}, index=pd.date_range('2024-02-01', periods=3))  # 2月3个交易日的收盘价
    q1_prices = pd.concat([jan_prices, feb_prices], keys=['1月', '2月'])  # 纵向拼接并用keys标识来源,形成两层行索引
    print(q1_prices)  # 查看带来源标识的拼接结果
    print(q1_prices.loc['1月'])  # 用外层标签取出1月子表

    预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):

                    收盘价
    1月 2024-01-01  10.0
       2024-01-02  10.5
       2024-01-03  11.0
    2月 2024-02-01  11.5
       2024-02-02  12.0
       2024-02-03  12.5
                 收盘价
    2024-01-01  10.0
    2024-01-02  10.5
    2024-01-03  11.0

    注意:以上为本题变式的独立代码;列表 12.1 对应的平台原始代码仍须原样输入教学平台,不要用本变式替换。

    回扣本章:对应本章小结“要点”第 1 条——concat 沿轴堆叠多个数据框,keys 可标识数据来源。

  4. 动手验证:对同两个表分别运行 pd.concat([a, b], axis=0)pd.concat([a, b], axis=1),再给后者加上 ignore_index=True,观察索引编号与 NaN 位置的变化。

    参考答案(点开前请先独立完成)

    解题思路:三个输出对照看。axis=0 纵向堆叠:行数相加(2+2=4 行),列名取两表并集,a 的行没有 yb 的行没有 x,都补 NaN;行索引原样保留,于是 r2 出现两次——这正是平台任务用位置切片“无缝衔接”拼回原表时索引仍连续的原因(两段行索引本来互不重叠)。axis=1 横向拼接:行索引取并集 r1、r2、r3r1yr3xNaN。加上 ignore_index=True 后要特别注意:ignore_index 重置的是沿拼接轴的索引,axis=1 的拼接轴是列轴,因此被重置的是列名(xy 变成 01),行索引 r1、r2、r3 原样保留,NaN 位置也不变;只有 axis=0 时它才会把行索引重编为 0、1、2、3。

    # 验证脚本:对比concat两种轴向及ignore_index的效果
    import pandas as pd  # 导入Pandas库
    
    table_a = pd.DataFrame({'x': [1, 2]}, index=['r1', 'r2'])  # 左表:x列,两行
    table_b = pd.DataFrame({'y': [3, 4]}, index=['r2', 'r3'])  # 右表:y列,行索引与左表部分重叠
    print(pd.concat([table_a, table_b], axis=0))  # 纵向堆叠:列名取并集,对不上的位置补NaN,行索引保留(r2出现两次)
    print(pd.concat([table_a, table_b], axis=1))  # 横向拼接:行索引取并集,r1缺y、r3缺x补NaN
    print(pd.concat([table_a, table_b], axis=1, ignore_index=True))  # 沿拼接轴(列轴)重编号:列名变0、1,行索引不变

    预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):

          x    y
    r1  1.0  NaN
    r2  2.0  NaN
    r2  NaN  3.0
    r3  NaN  4.0
          x    y
    r1  1.0  NaN
    r2  2.0  3.0
    r3  NaN  4.0
          0    1
    r1  1.0  NaN
    r2  2.0  3.0
    r3  NaN  4.0

    回扣本章:对应本章小结“要点”第 1、4 条与“易错点”第 1 条——concat 默认 join='outer',两表列或索引不完全一致时结果中会出现 NaNignore_index 只重置沿拼接轴的索引。